make sure date later than now will now be publish

This commit is contained in:
polv committed 2020-12-18 16:32:43 +07:00
1 parent 9ea60c68c7
commit d8161be401
47 files changed
+25 -11589

No files matched your search

View File
Whitespace-only changes.
+4
View File
@@ -0,0 +1,4 @@
{
"editor.defaultFormatter": "rvest.vs-code-prettier-eslint",
"editor.formatOnSave": true
}
+15 -22
View File
@@ -1,30 +1,23 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
Almost classless Next.js template (with TypeScript, ESLint, Prettier) powered by awsm.css
## Getting Started
Tested with Yarn and Node.js 12.
First, run the development server:
## Needed files
```bash
npm run dev
# or
yarn dev
You will need these files and folders, either at `./content/` or `../../`
```sh
_post/ # => will be parsed to /build/db.loki and /build/idx.json
_media/ # => will be copied to /public/media/
theme.yml # => will be parsed to /build/theme.json
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
The structure of `theme.yml` can be seen at [/server/theme.ts](/server/theme.ts).
You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file.
Markdown files should be put `_post/`, with YAML frontmatter required keys - `title`. `date` < NOW is required to be "visible" in the listing (at `${baseUrl}/blog`, `${baseUrl}/tag`). For more information, please see [/server/db.ts](/server/db.ts).
## Learn More
## Features
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/import?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
- Search engine with `lunr.js`
- Commenting engine with `remark42`
- Analytics with `plausible analytics`
+2 -2
View File
@@ -1,5 +1,5 @@
{
"name": "@zhdiary/next",
"name": "@patarapolw/next-awsm",
"version": "0.1.0",
"private": true,
"scripts": {
@@ -58,4 +58,4 @@
"style": "module"
}
}
}
}
+1 -1
View File
@@ -29,7 +29,7 @@ export const getStaticPaths: GetStaticPaths = async () => {
paths: Array(
Math.ceil(
dbPost.find({
date: { $exists: true } as any
date: { $lt: new Date().getTime() }
}).length / 5
) - 1
)
+3 -1
View File
@@ -35,7 +35,9 @@ export const getStaticPaths: GetStaticPaths = async () => {
await initDatabase()
const tMap = new Map<string, number>()
dbPost.find().map(({ tag = [] }) => {
dbPost.find({
date: { $lt: new Date().getTime() }
}).map(({ tag = [] }) => {
return tag.map((t) => {
return tMap.set(t, (tMap.get(t) || 0) + 1)
})
-8
View File
@@ -1,8 +0,0 @@
{
"presets": ["@babel/preset-typescript"],
"plugins": [
"@babel/plugin-proposal-class-properties",
"@babel/plugin-transform-object-assign",
"@babel/plugin-proposal-object-rest-spread"
]
}
-13
View File
@@ -1,13 +0,0 @@
# editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
-21
View File
@@ -1,21 +0,0 @@
module.exports = {
root: true,
env: {
browser: true,
node: true,
},
extends: [
'@nuxtjs/eslint-config-typescript',
'prettier',
'prettier/vue',
'plugin:prettier/recommended',
'plugin:nuxt/recommended',
],
plugins: ['prettier'],
// add your custom rules here
rules: {
'require-await': 0,
'vue/no-v-html': 0,
'import/order': 0,
},
}
-94
View File
@@ -1,94 +0,0 @@
# Created by .ignore support plugin (hsz.mobi)
### Node template
# Logs
/logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# Nuxt generate
dist
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless
# IDE / Editor
.idea
# Service worker
sw.*
# macOS
.DS_Store
# Vim swap files
*.swp
/build/
/static/media/
/*-dist/
-4
View File
@@ -1,4 +0,0 @@
server/
scripts/
lambda/
lambda-dist/
-4
View File
@@ -1,4 +0,0 @@
{
"semi": false,
"singleQuote": true
}
-4
View File
@@ -1,4 +0,0 @@
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "rvest.vs-code-prettier-eslint"
}
-20
View File
@@ -1,20 +0,0 @@
# zhdiary
## Build Setup
```bash
# install dependencies
$ yarn install
# serve with hot reload at localhost:3000
$ yarn dev
# build for production and launch server
$ yarn build
$ yarn start
# generate static project
$ yarn generate
```
For detailed explanation on how things work, check out [Nuxt.js docs](https://nuxtjs.org).
@@ -1,3 +0,0 @@
export default function () {
return { x: 0, y: 0 }
}
-6
View File
@@ -1,6 +0,0 @@
interface IPostTeaser {
title: string
path: string
date?: string
excerptHtml: string
}
-3
View File
@@ -1,3 +0,0 @@
import type { ITheme } from '~/server/theme'
export const THEME: ITheme = JSON.parse(process.env.THEME!)
-48
View File
@@ -1,48 +0,0 @@
/* eslint-disable camelcase */
import { THEME } from './global'
export type Theme = 'light' | 'dark'
export interface CommentsConfig {
host: string
site_id: string
url?: string
max_shown_comments?: number
theme?: Theme
page_title?: string
node?: string | HTMLElement
locale?: string
// eslint-disable-next-line @typescript-eslint/no-explicit-any
__colors__?: any
}
declare global {
interface Window {
remark_config: CommentsConfig
REMARK42: {
changeTheme(theme: Theme): void
destroy?: () => void
createInstance: (
remark_config: CommentsConfig
) =>
| {
changeTheme(theme: Theme): void
destroy(): void
}
| undefined
}
}
}
if (process.browser && THEME?.comment?.remark42) {
;(function (c) {
for (let i = 0; i < c.length; i++) {
const d = document
const s = d.createElement('script')
s.src = THEME.comment.remark42.host + '/web/' + c[i] + '.js'
s.defer = true
;(d.head || d.body).appendChild(s)
}
})(['embed'])
}
-115
View File
@@ -1,115 +0,0 @@
<template>
<article>
<post-teaser v-for="p in posts" :key="p.path" :post="p"></post-teaser>
<nav v-if="count > 5">
<ul>
<li v-if="page > 1">
<nuxt-link :to="makePageUrl(1)" aria-label="page 1">1</nuxt-link>
</li>
<li v-if="page > 3">
<span> &hellip; </span>
</li>
<li v-if="page > 2">
<nuxt-link
:to="makePageUrl(page - 1)"
:aria-label="`page ${page - 1}`"
>
{{ page - 1 }}
</nuxt-link>
</li>
<li>
<span>
{{ page }}
</span>
</li>
<li v-if="page < total - 1">
<nuxt-link
:to="makePageUrl(page + 1)"
:aria-label="`page ${page + 1}`"
>
{{ page + 1 }}
</nuxt-link>
</li>
<li v-if="page < total - 2">
<span> &hellip; </span>
</li>
<li v-if="page < total">
<nuxt-link :to="makePageUrl(total)" :aria-label="`page ${total}`">
{{ total }}
</nuxt-link>
</li>
</ul>
</nav>
</article>
</template>
<script lang="ts">
import { Vue, Component, Prop, Watch } from 'nuxt-property-decorator'
@Component
export default class PostQuery extends Vue {
@Prop({ required: true }) posts!: IPostTeaser[]
@Prop({ required: true }) count!: number
readonly defaults = {
posts: this.posts,
count: this.count,
}
get page() {
return parseInt((this.$route.query.page as string) || '1')
}
get total() {
return Math.ceil(this.count / 5)
}
makePageUrl(p: number) {
let path = this.$route.path
if (!(path.startsWith('/tag') || path.startsWith('/blog'))) {
path = '/blog'
}
path = path.replace(/\d+$/, '').replace(/\/$/, '')
if (p > 1) {
path = path + '/' + p
}
return this.$router.resolve({
path,
query: this.$route.query,
}).href
}
@Watch('$route.params.page')
@Watch('$route.params.tag')
async updatePosts() {
if (this.$route.query.q) {
const ps = await this.$axios.$post(
'/.netlify/functions/search',
undefined,
{
params: {
q: this.$route.query.q,
page: this.$route.params.page,
tag: this.$route.params.tag,
},
}
)
this.count = ps.count
this.$set(this, 'posts', ps.result)
} else {
this.count = this.defaults.count
this.$set(this, 'posts', this.defaults.posts)
}
}
}
</script>
-48
View File
@@ -1,48 +0,0 @@
<template>
<section>
<h2>
<nuxt-link :to="'/post/' + post.path">
{{ post.title }}
</nuxt-link>
</h2>
<small>
<span>
by
<a
v-if="theme.author.url"
:href="theme.author.url"
target="_blank"
rel="noopener"
>
{{ theme.author.name }}
</a>
<span v-else>
{{ theme.author.name }}
</span>
</span>
<span v-if="post.date" style="float: right">
{{ post.date | formatDate }}
</span>
</small>
<article v-html="post.excerptHtml"></article>
</section>
</template>
<script lang="ts">
import { Vue, Component, Prop } from 'nuxt-property-decorator'
import { THEME } from '~/assets/global'
@Component
export default class PostTeaser extends Vue {
@Prop() post!: IPostTeaser
theme = THEME
}
</script>
<style scoped>
article {
margin-top: 2em;
}
</style>
-50
View File
@@ -1,50 +0,0 @@
import { Handler } from 'aws-lambda'
import lunr, { Index } from 'lunr'
import idxJson from '../build/idx.json'
import { dbPost, initDatabase } from '../server/db'
let idx: Index
export const handler: Handler = async (evt) => {
const { q = '', tag, page: _page = '1' } = evt.queryStringParameters || {}
const perPage = 5
const offset = (parseInt(_page) - 1) * perPage
idx = idx || lunr.Index.load(idxJson)
await initDatabase()
let cond = dbPost.chain()
if (q) {
cond = cond.find({
path: { $in: idx.search(q).map(({ ref }) => ref) },
})
}
if (tag) {
cond = cond.find({
tag: { $contains: tag },
})
}
const count = cond.branch().count()
const result = cond
.offset(offset)
.limit(5)
.data()
.map(({ title, path, date, excerptHtml }) => ({
title,
path,
date,
excerptHtml,
}))
return {
statusCode: 200,
body: JSON.stringify({
count,
result,
}),
}
}
-74
View File
@@ -1,74 +0,0 @@
<template>
<main>
<header>
<h1>{{ theme.title }}</h1>
<nav>
<ul>
<li>
<nuxt-link to="/">Home</nuxt-link>
</li>
<li v-for="(n, i) in theme.nav || []" :key="i">
<nuxt-link v-if="n.to" :to="n.to">{{ n.name }}</nuxt-link>
<a v-else :href="n.href" target="_blank" rel="noopener">
{{ n.name }}
</a>
</li>
</ul>
<form @submit.prevent="doSearch">
<input v-model="q" type="search" placeholder="Search..." />
<div class="icon">🔎</div>
</form>
</nav>
</header>
<Nuxt />
</main>
</template>
<script lang="ts">
import { Vue, Component } from 'nuxt-property-decorator'
import { THEME } from '~/assets/global'
@Component
export default class DefaultLayout extends Vue {
theme = THEME
q = this.$route.query.q || ''
doSearch() {
let path = this.$route.path
if (['/blog', '/tag'].includes(path)) {
path = '/blog'
}
this.$router.push({
path,
query: {
q: this.q,
},
})
}
}
</script>
<style scoped>
nav ul {
display: inline-block;
}
form {
float: right;
max-width: 300px;
position: relative;
}
form .icon {
position: absolute;
right: 5px;
top: 5px;
}
input {
padding-right: 1.5em;
}
</style>
-19
View File
@@ -1,19 +0,0 @@
<template>
<div>
<section>
<h2 v-if="error.statusCode === 404">Page not found</h2>
<pre v-else style="width: 100%">{{ error }}</pre>
</section>
<section>
<NuxtLink to="/">Home page</NuxtLink>
</section>
</div>
</template>
<script>
export default {
// eslint-disable-next-line vue/require-prop-types
props: ['error'],
layout: 'error', // you can set a custom layout for the error page
}
</script>
-8
View File
@@ -1,8 +0,0 @@
# MIDDLEWARE
**This directory is not required, you can delete it if you don't want to use it.**
This directory contains your application middleware.
Middleware let you define custom functions that can be run before rendering either a page or a group of pages.
More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/routing#middleware).
-3
View File
@@ -1,3 +0,0 @@
[build]
functions = "lambda-dist"
publish = "dist"
-107
View File
@@ -1,107 +0,0 @@
import { NuxtConfig } from '@nuxt/types'
import { SERVER_PORT, runServer } from './server/cli'
import { getTheme } from './server/theme'
const config = async (): Promise<NuxtConfig> => {
await runServer()
const theme = getTheme()
return {
target: 'static',
// Global page headers (https://go.nuxtjs.dev/config-head)
head: {
title: theme.title,
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{
hid: 'description',
name: 'description',
content: theme.description || '',
},
],
link: [
{ rel: 'icon', type: 'image/x-icon', href: '/media/favicon.ico' },
{
rel: 'stylesheet',
href: theme.awsm
? `https://unpkg.com/awsm.css/dist/awsm_theme_${theme.awsm}.min.css`
: 'https://unpkg.com/awsm.css/dist/awsm.min.css',
},
],
script: [
...(theme.analytics?.plausible
? [
{
src: 'https://plausible.io/js/plausible.js',
async: true,
defer: true,
'data-domain': theme.analytics?.plausible,
},
]
: []),
{
src:
'https://unpkg.com/@webcomponents/webcomponentsjs/webcomponents-loader.js',
defer: true,
},
{
type: 'module',
src: '/webcomponents.js',
},
],
},
// Global CSS (https://go.nuxtjs.dev/config-css)
css: [],
// Plugins to run before rendering page (https://go.nuxtjs.dev/config-plugins)
plugins: ['~/plugins/axios-fix.ts', '~/plugins/format.ts'],
// Auto import components (https://go.nuxtjs.dev/config-components)
components: true,
// Modules for dev and build (recommended) (https://go.nuxtjs.dev/config-modules)
buildModules: [
// https://go.nuxtjs.dev/typescript
'@nuxt/typescript-build',
],
// Modules (https://go.nuxtjs.dev/config-modules)
modules: ['@nuxtjs/axios'],
axios: {
proxy: true,
},
proxy: {
'/.netlify/functions': 'http://localhost:9000',
'/api': `http://localhost:${SERVER_PORT}`,
},
// Build Configuration (https://go.nuxtjs.dev/config-build)
build: {},
env: {
THEME: JSON.stringify(theme),
SERVER_PORT: SERVER_PORT.toString(),
},
hooks: {
generate: {
done() {
/**
* Do not kill
*/
// try {
// execSync(
// `lsof -i tcp:${SERVER_PORT} | grep LISTEN | awk '{print $2}' | xargs kill -9`
// )
// } catch (_) {}
process.exit(0)
},
},
},
}
}
export default config
-85
View File
@@ -1,85 +0,0 @@
{
"name": "@patarapolw/nuxt-awsm",
"version": "0.1.0",
"private": true,
"scripts": {
"start": "nuxt-ts start",
"generate": "nuxt-ts generate",
"lint": "eslint --ext .ts,.js,.vue --ignore-path .gitignore .",
"ts": "ts-node -O '{\"module\":\"commonjs\"}'",
"db:make": "yarn ts scripts/make.ts",
"dev": "run-p 'dev:**'",
"dev:nuxt": "nuxt-ts",
"dev:lambda": "netlify-lambda serve lambda",
"prebuild": "yarn db:make",
"build": "run-p 'build:**'",
"build:nuxt": "nuxt-ts generate",
"build:lambda": "netlify-lambda build lambda",
"postinstall": "netlify-lambda install"
},
"dependencies": {
"@nuxt/typescript-runtime": "^2.0.0",
"@nuxtjs/axios": "^5.12.4",
"@patarapolw/scope-css": "^1.3.0",
"@types/aws-lambda": "^8.10.66",
"@types/express": "^4.17.9",
"@types/js-yaml": "^3.12.5",
"@types/lokijs": "^1.5.3",
"@types/lunr": "^2.3.3",
"@types/morgan": "^1.9.2",
"cheerio": "^1.0.0-rc.3",
"core-js": "^3.6.5",
"express": "^4.17.1",
"js-yaml": "^3.14.1",
"lokijs": "^1.5.11",
"lunr": "^2.3.9",
"morgan": "^1.10.0",
"netlify-lambda": "^2.0.2",
"nuxt": "^2.14.6",
"nuxt-property-decorator": "^2.8.8",
"showdown": "^1.9.1",
"zod": "^1.11.11"
},
"devDependencies": {
"@babel/plugin-proposal-class-properties": "^7.12.1",
"@babel/plugin-proposal-object-rest-spread": "^7.12.1",
"@babel/plugin-transform-object-assign": "^7.12.1",
"@babel/preset-typescript": "^7.12.1",
"@nuxt/types": "^2.14.6",
"@nuxt/typescript-build": "^2.0.3",
"@nuxtjs/eslint-config": "^3.1.0",
"@nuxtjs/eslint-config-typescript": "^3.0.0",
"@nuxtjs/eslint-module": "^2.0.0",
"@types/cheerio": "^0.22.23",
"@types/ejs": "^3.0.5",
"@types/fs-extra": "^9.0.5",
"@types/rimraf": "^3.0.0",
"@types/showdown": "^1.9.3",
"babel-eslint": "^10.1.0",
"dotenv": "^8.2.0",
"ejs": "^3.1.5",
"eslint": "^7.10.0",
"eslint-config-prettier": "^6.12.0",
"eslint-plugin-nuxt": "^1.0.0",
"eslint-plugin-prettier": "^3.1.4",
"import-sort-parser-typescript": "^6.0.0",
"nodejieba": "^2.5.1",
"npm-run-all": "^4.1.5",
"prettier": "^2.1.2",
"ts-node": "^9.1.1",
"typescript": "4.0.5"
},
"importSort": {
".ts, .tsx, .config.ts": {
"parser": "typescript",
"style": "module"
}
},
"engines": {
"node": "12",
"npm": "please-use-yarn",
"yarn": "1",
"pnpm": "please-use-yarn"
},
"engineStrict": true
}
-26
View File
@@ -1,26 +0,0 @@
<template>
<post-query :posts="posts" :count="count" />
</template>
<script lang="ts">
import { Vue, Component } from 'nuxt-property-decorator'
@Component<BlogPagePaged>({
async asyncData({ $axios, params }) {
const { result, count } = await $axios.$get('/api/q', {
params: {
page: params.page,
},
})
return {
posts: result,
count,
}
},
})
export default class BlogPagePaged extends Vue {
posts: IPostTeaser[] = []
count = 0
}
</script>
-22
View File
@@ -1,22 +0,0 @@
<template>
<post-query :posts="posts" :count="count" />
</template>
<script lang="ts">
import { Vue, Component } from 'nuxt-property-decorator'
@Component<BlogPage>({
async asyncData({ $axios }) {
const { result, count } = await $axios.$get('/api/q')
return {
posts: result,
count,
}
},
})
export default class BlogPage extends Vue {
posts: IPostTeaser[] = []
count = 0
}
</script>
-9
View File
@@ -1,9 +0,0 @@
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
middleware: (ctx) => {
ctx.redirect('/blog')
},
})
</script>
-173
View File
@@ -1,173 +0,0 @@
<template>
<section>
<section>
<h2>
{{ post.title }}
</h2>
<small>
<span>
by
<a
v-if="theme.author.url"
:href="theme.author.url"
target="_blank"
rel="noopener"
>
{{ theme.author.name }}
</a>
<span v-else>
{{ theme.author.name }}
</span>
</span>
<span v-if="post.date" style="float: right">
{{ post.date | formatDate }}
</span>
</small>
</section>
<section v-html="post.contentHtml"></section>
<section class="tags-container">
<span>Tags:</span>
<span v-for="t in post.tag || []" :key="t">
<nuxt-link :to="`/tag/${t}`">
{{ t }}
</nuxt-link>
</span>
</section>
<footer v-if="remark42settings">
<div ref="remark42" />
</footer>
</section>
</template>
<script lang="ts">
import { Vue, Component, Watch } from 'nuxt-property-decorator'
import { THEME } from '~/assets/global'
import '~/assets/remark42'
@Component<PostPage>({
async asyncData({ $axios, params }) {
const r = await $axios.$get('/api', {
params: {
path: params.pathMatch,
},
})
return {
post: r,
}
},
head() {
const { title: _title, content, tag, image } = this.post
const title = `${_title} - ${THEME.title}`
const description = content.substr(0, 140)
return {
title,
meta: [
{
hid: 'description',
name: 'description',
content: description,
},
...(tag
? [
{
hid: 'keywords',
name: 'keywords',
content: tag.join(','),
},
]
: []),
{
hid: 'og:title',
property: 'og:title',
content: title,
},
{
hid: 'og:description',
property: 'og:description',
content: description,
},
{
hid: 'twitter:title',
property: 'twitter:title',
content: title,
},
{
hid: 'twitter:description',
property: 'twitter:description',
content: description,
},
...(image
? [
{
hid: 'og:image',
property: 'og:image',
content: image,
},
{
hid: 'twitter:image',
property: 'twitter:image',
content: image,
},
]
: []),
],
}
},
mounted() {
this.initRemark42()
},
beforeDestroy() {
if (this.remark42Instance) {
this.remark42Instance.destroy()
}
},
})
export default class PostPage extends Vue {
post!: {
date?: string
title: string
image?: string
tag?: string[]
category?: string[]
content: string
}
theme = THEME
remark42settings = THEME.comment?.remark42 || null
remark42Instance: any = null
@Watch('$route.params.pathMatch')
initRemark42() {
this.$nextTick(() => {
if (
this.$refs.remark42 &&
process.browser &&
this.remark42settings &&
window.REMARK42
) {
if (this.remark42Instance) {
this.remark42Instance.destroy()
}
this.remark42Instance = window.REMARK42.createInstance({
node: this.$refs.remark42 as HTMLElement,
host: this.remark42settings.host,
site_id: this.remark42settings.siteId,
})
}
})
}
}
</script>
<style scoped>
.tags-container > span + span {
margin-left: 0.6em;
}
</style>
-27
View File
@@ -1,27 +0,0 @@
<template>
<post-query :posts="posts" :count="count" />
</template>
<script lang="ts">
import { Vue, Component } from 'nuxt-property-decorator'
@Component<TagPagePaged>({
async asyncData({ $axios, params }) {
const { result, count } = await $axios.$get('/api/q', {
params: {
page: params.page,
tag: params.tag,
},
})
return {
posts: result,
count,
}
},
})
export default class TagPagePaged extends Vue {
posts: IPostTeaser[] = []
count = 0
}
</script>
-26
View File
@@ -1,26 +0,0 @@
<template>
<post-query :posts="posts" :count="count" />
</template>
<script lang="ts">
import { Vue, Component } from 'nuxt-property-decorator'
@Component<TagPage>({
async asyncData({ $axios, params }) {
const { result, count } = await $axios.$get('/api/q', {
params: {
tag: params.tag,
},
})
return {
posts: result,
count,
}
},
})
export default class TagPage extends Vue {
posts: IPostTeaser[] = []
count = 0
}
</script>
-32
View File
@@ -1,32 +0,0 @@
import { Plugin } from '@nuxt/types'
const plugin: Plugin = ({ $axios, error }) => {
$axios.defaults.baseURL =
typeof location !== 'undefined'
? location.origin
: `http://localhost:${process.env.SERVER_PORT}`
$axios.interceptors.response.use(
(r) => {
if (r.data?.error) {
error({
statusCode: r.status,
message: r.data.error,
})
throw new Error(r.data.error)
}
return r
},
(err) => {
error({
statusCode: err.response.status,
message: err.toJSON(),
})
return Promise.reject(err)
}
)
}
export default plugin
-12
View File
@@ -1,12 +0,0 @@
import Vue from 'vue'
Vue.filter('formatDate', (s?: string | number) => {
return s
? new Date(s).toLocaleDateString([], {
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'short',
})
: ''
})
-19
View File
@@ -1,19 +0,0 @@
import fs from 'fs'
import path from 'path'
import dotenv from 'dotenv'
import { THEME_FILENAME } from '../server/theme'
dotenv.config({ path: '../../.env' })
dotenv.config()
export const CONTENT_PATH = path.resolve(
process.env.CONTENT_PATH ||
(fs.existsSync('../../_post') ? '../..' : 'content')
)
export const srcPath = (
root: '_post' | '_media' | typeof THEME_FILENAME,
...ps: string[]
): string => path.join(CONTENT_PATH, root, ...ps)
-154
View File
@@ -1,154 +0,0 @@
import crypto from 'crypto'
import scopeCss from '@patarapolw/scope-css'
import cheerio from 'cheerio'
import ejs from 'ejs'
import showdown from 'showdown'
export class MakeHtml {
$ = cheerio.load('')
mdConverter = new showdown.Converter({
noHeaderId: true,
parseImgDimensions: true,
simplifiedAutoLink: true,
// literalMidWordUnderscores: true,
strikethrough: true,
tables: true,
tasklists: true,
// simpleLineBreaks: true,
// openLinksInNewWindow: true,
backslashEscapesHTMLTags: true,
emoji: true,
underline: true,
disableForced4SpacesIndentedSublists: true,
metadata: true,
})
constructor() {
this.mdConverter.addExtension(
{
type: 'lang',
regex: /\\\n/g,
replace: '<br />',
},
'commonmark-simple-linebreak'
)
/**
* ! For some reasons default underscore italic fails to render...
*/
this.mdConverter.addExtension(
{
type: 'lang',
regex: /([^\\])_{1}(.*?[^\\])_{1}/g,
replace: '$1<i>$2</i>',
},
'underscore-italic'
)
/**
* ! This is the only way to preserve syntax highlighting, while output multiline strings correctly
*/
this.mdConverter.addExtension(
{
type: 'lang',
regex: /(^|\n)```html parsed([^]+)\n```\n/g,
replace: (_: string, p1: string, p2: string) => {
return (
p1 +
ejs.render(p2, {
rmWhitespace: true,
beautify: false,
})
)
},
},
'html-parsed'
)
}
async parse(
filename: string
): Promise<{
html: string
text: string
}> {
const id =
'md-' +
crypto
.createHash('sha256')
.update(filename)
.digest()
.toString('hex')
.substr(0, 8)
const $div = this.$(`<div id="${id}">`)
$div.html(await this.getHTML(filename))
$div.find('style').each((_, el) => {
const $el = this.$(el)
const html = $el.html() || ''
if (html.trim()) {
$el.html(scopeCss(html, `#${id}`))
}
})
$div.find('a').each((_, el) => {
const $el = this.$(el)
const href = $el.attr('href') || ''
if (/^https?:\/\//.test(href)) {
$el.attr({
rel: 'noopener',
target: '_blank',
})
}
})
return {
html: this.$('<div>').append($div).html() || '',
text: $div.text().trim(),
}
}
private async getHTML(filename: string) {
return this.mdConverter.makeHtml(
await ejs.renderFile(filename, {
youtube: (id: string) =>
ejs.render(
/* html */ `
<div style="position:relative;padding-top:56.25%;">
<iframe src="https://www.youtube.com/embed/${id}"
frameborder="0"
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
style="position:absolute;top:0;left:0;width:100%;height:100%"></iframe>
</div>`,
{ rmWhitespace: true, beautify: false }
),
xCard: (opts: {
href: string
image?: string
title: string
description?: string
}) =>
this.$('<div>')
.append(
this.$('<x-card>')
.attr(opts)
.append(
this.$('<a>')
.attr({
href: opts.href,
rel: 'noopener',
target: '_blank',
})
.text(opts.title)
)
)
.html(),
})
)
}
}
-86
View File
@@ -1,86 +0,0 @@
import fg from 'fast-glob'
import fs from 'fs-extra'
import yaml from 'js-yaml'
import lunr from 'lunr'
import jieba from 'nodejieba'
import rimraf from 'rimraf'
import { IPost, db, dbPost, initDatabase, zMatter, zPost } from '../server/db'
import { buildPath, dstMediaPath } from '../server/dir'
import { THEME_FILENAME } from '../server/theme'
import { srcPath } from './dir'
import { MakeHtml } from './make-html'
async function main() {
rimraf.sync(dstMediaPath())
rimraf.sync(buildPath())
await fs.copy(srcPath('_media'), dstMediaPath()).catch((e) => {
// eslint-disable-next-line no-console
console.error(e)
})
await fs.ensureDir(buildPath())
await fs.copyFile(srcPath(THEME_FILENAME), buildPath(THEME_FILENAME))
const files = await fg('**/*.md', {
cwd: srcPath('_post'),
})
const makeHtml = new MakeHtml()
const rawJson: IPost[] = []
await initDatabase()
await Promise.all(
files.map(async (f) => {
const header = zMatter.parse(
yaml.safeLoad(
fs.readFileSync(srcPath('_post', f), 'utf-8').split(/\n---\n/)[0]
)
)
const { html, text } = await makeHtml.parse(srcPath('_post', f))
const p: IPost = {
...header,
date: header.date ? header.date.getTime() : undefined,
createdAt: header.createdAt ? header.createdAt.getTime() : undefined,
updatedAt: header.updatedAt ? header.updatedAt.getTime() : undefined,
path: f.replace(/\.mdx?$/, ''),
excerptHtml: html.split(/<!-- excerpt(?:_separator)? -->/)[0],
contentHtml: html,
content: text,
}
dbPost.insert(zPost.parse(p))
rawJson.push({
...p,
content: jieba.cutForSearch(p.content).join(' '),
})
})
)
db.save()
fs.writeFileSync(
buildPath('idx.json'),
JSON.stringify(
lunr(function () {
this.ref('path')
this.field('title', { boost: 5 })
this.field('tag', { boost: 5 })
this.field('category', { boost: 5 })
this.field('content')
rawJson.map((doc) => {
return this.add(doc)
})
})
)
)
}
if (require.main === module) {
main()
}
-119
View File
@@ -1,119 +0,0 @@
import http from 'http'
import express from 'express'
import lunr from 'lunr'
import morgan from 'morgan'
import * as z from 'zod'
import idxJson from '../build/idx.json'
import { dbPost, initDatabase } from './db'
export const SERVER_PORT = 5000
export async function runServer() {
await initDatabase()
const app = express()
if (process.env.NODE_ENV === 'development') {
app.use(morgan('common'))
}
const router = express.Router()
app.use('/api', router)
const idx = lunr.Index.load(idxJson)
router.get('/', (req, res) => {
const { path } = z
.object({
path: z.string(),
})
.parse(req.query)
const r = dbPost.findOne({ path })
if (!r) {
res.sendStatus(404)
return
}
res.json(r)
})
router.get('/q', (req, res) => {
const { q, tag, page: _page = '1' } = z
.object({
q: z.string().optional(),
tag: z.string().optional(),
page: z
.string()
.refine((s) => /^\d+$/.test(s))
.optional(),
})
.parse(req.query)
const perPage = 5
const offset = (parseInt(_page) - 1) * perPage
let cond = dbPost.chain().find({
date: { $exists: true } as any,
})
if (q) {
cond = cond.find({
path: { $in: idx.search(q).map(({ ref }) => ref) },
})
}
if (tag) {
cond = cond.find({
tag: { $contains: tag },
})
}
cond = cond.simplesort('date', { desc: true })
const count = cond.branch().count()
const result = cond.offset(offset).limit(5).data()
res.json({
count,
result,
})
})
app.use((err: Error, _: express.Request, res: express.Response) => {
// eslint-disable-next-line no-console
console.error(err.stack)
res.status(500).send(err)
})
await new Promise<http.Server | Error>((resolve) => {
const srv = app
.listen(SERVER_PORT, () => {
// eslint-disable-next-line no-console
console.info(
`serverMiddleware is running at http://localhost:${SERVER_PORT}`
)
resolve(srv)
})
.once('error', (e) => {
/**
* Nuxt generate will try to spawn two process on the same SERVER_PORT.
*
* One of which will inevitably fail.
*
* ! DO NOT reject()
*/
resolve(e)
})
.once('close', () => {
// eslint-disable-next-line no-console
console.info('serverMiddleware is closing')
})
})
}
if (require.main === module) {
// eslint-disable-next-line no-console
runServer().catch(console.error)
}
-87
View File
@@ -1,87 +0,0 @@
/* eslint-disable import/no-mutable-exports */
import Loki, { Collection } from 'lokijs'
import * as z from 'zod'
import { buildPath } from './dir'
const _zMatter = {
priority: z.number().optional(),
title: z.string(),
image: z.string().optional(),
tag: z.array(z.string()).optional(),
category: z.array(z.string()).optional(),
}
export const zMatter = z.object({
..._zMatter,
date: z.date().optional(),
createdAt: z.date().optional(),
updatedAt: z.date().optional(),
})
export type IMatter = z.infer<typeof zMatter>
const zMatterSerialized = z.object({
..._zMatter,
date: z.number().optional(),
createdAt: z.number().optional(),
updatedAt: z.number().optional(),
})
export type IMatterSerialized = z.infer<typeof zMatterSerialized>
export const zPost = z
.object({
path: z.string(),
content: z.string(),
excerptHtml: z.string(),
contentHtml: z.string(),
})
.merge(zMatterSerialized)
export type IPost = z.infer<typeof zPost>
export interface IPostSearch {
path: string
title: string
tag?: string[]
category?: string[]
/**
* Cleaned and jieba-separated markdown
*/
content: string
}
export let db: Loki
export let dbPost: Collection<IPost>
export async function initDatabase() {
if (db) {
return
}
return new Promise<void>((resolve) => {
db = new Loki(buildPath('data.loki'), {
autoload: true,
autoloadCallback: () => {
dbPost = db.getCollection('post')
if (!dbPost) {
dbPost = db.addCollection('post', {
unique: ['path'],
indices: [
'date',
'createdAt',
'updatedAt',
'priority',
'tag',
'category',
],
})
}
resolve()
},
})
})
}
-7
View File
@@ -1,7 +0,0 @@
import path from 'path'
export const buildPath = (...ps: string[]): string =>
path.join(__dirname, '../build', ...ps)
export const dstMediaPath = (...ps: string[]): string =>
path.join(__dirname, '../static/media', ...ps)
-62
View File
@@ -1,62 +0,0 @@
import fs from 'fs'
import yaml from 'js-yaml'
import * as z from 'zod'
import { buildPath } from './dir'
export const THEME_FILENAME = 'theme.yml'
export const zTheme = z
.object({
title: z.string(),
awsm: z.string().optional(),
description: z.string().optional(),
keywords: z.array(z.string()).optional(),
nav: z
.array(
z
.object({
name: z.string(),
to: z.string(),
})
.or(
z.object({
name: z.string(),
href: z.string(),
})
)
)
.optional(),
author: z
.object({
url: z.string().optional(),
email: z.string().optional(),
name: z.string(),
})
.nonstrict(),
analytics: z
.object({
plausible: z.string().optional(),
})
.optional(),
comment: z
.object({
remark42: z
.object({
host: z.string(),
siteId: z.string(),
})
.optional(),
})
.optional(),
})
.nonstrict()
export type ITheme = z.infer<typeof zTheme>
export function getTheme() {
return zTheme.parse(
yaml.safeLoad(fs.readFileSync(buildPath(THEME_FILENAME), 'utf-8'))
)
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

-11
View File
@@ -1,11 +0,0 @@
/* global WebComponents */
WebComponents.waitFor(() => {
// At this point we are guaranteed that all required polyfills have
// loaded, and can use web components API's.
// The standard pattern is to load element definitions that call
// `customElements.define` here.
// Note: returning the import's promise causes the custom elements
// polyfill to wait until all definitions are loaded and then upgrade
// the document in one batch, for better performance.
return import('https://unpkg.com/@patarapolw/make-html-x-card')
})
-10
View File
@@ -1,10 +0,0 @@
# STORE
**This directory is not required, you can delete it if you don't want to use it.**
This directory contains your Vuex Store files.
Vuex Store option is implemented in the Nuxt.js framework.
Creating a file in this directory automatically activates the option in the framework.
More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/vuex-store).
-22
View File
@@ -1,22 +0,0 @@
{
"compilerOptions": {
"target": "ES2018",
"module": "ESNext",
"moduleResolution": "Node",
"lib": ["ESNext", "ESNext.AsyncIterable", "DOM"],
"esModuleInterop": true,
"allowJs": true,
"sourceMap": true,
"strict": true,
"noEmit": true,
"experimentalDecorators": true,
"baseUrl": ".",
"paths": {
"~/*": ["./*"],
"@/*": ["./*"]
},
"types": ["@types/node", "@nuxt/types", "@nuxtjs/axios"],
"resolveJsonModule": true
},
"exclude": ["node_modules", ".nuxt", "dist"]
}
File diff suppressed because it is too large. Load diff