Roselia-Blog is a blog engine. Its front-end is mostly written in TypeScript and Vue, while its back-end is written in Python.
English | 简体中文
These are steps you should take after cloning this repo.
Python 3.6+(because of string interpolations).NodeJS(to compile the front-end).Yarn pkgThe package manager for NodeJS.
Then install the dependencies via pip install -r requirements.txt.
Then change current work dir to ./frontend and execute yarn.
There are three configs you need to change based on your conditions. In general, you do not need to change configs with a default value except blog title, motto and link.
In api_server/config.py you could change:
BLOG_LINK: The link to your blog webpage.BLOG_INFO: Change title, motto as you like. (For server-side static page rendering.)DEBUG: Make sure it isFalseif you want to run it in production.ANTI_SEO: Disable SEO optimization if this isTrue. If so, contents won’t be rendered per user request. Note that spiders which might execute user JavaScript like Google may also get the content.HOST: The listening host, default0.0.0.0.PORT: The listening port, default 5000.DB_PATH: The database address,can be SQL addresses.UPLOAD_DIR: The image upload directory. Make it empty to disable directly upload images to this server.
In api_server/secret.py you could change:
APP_KEY&APP_SALT: The application key and salt for token generation. Change it togen_key()to generate a random key every launch, or change it to a customstrvalue.GITHUB_CLIENT_ID&GITHUB_CLIENT_SECRET: the id and secret for GitHub OAuth, keep it empty to disable it.MICROSOFT_CLIENT_ID&MICROSOFT_CLIENT_SECRET: the id and secret for MSA OAuth, keep it empty to disable it.CHEVERETO_API_KEY&CHEVERETO_API_ENDPOINT: the api enpoint and key for uploading images to the chevereto service. Keep them empty to disable this upload channel.SM_MS_API_TOKEN: The api token to upload images tosm.msimage hosting server. Keep it empty to disable this channel.
In frontend/src/common/config.js, you could change (in bottom export default clause):
title: The title of blog.motto: The motto of blog.apiBase: The api base URL of the site, default/api.theme: The theme of the blog.enableRoseliaScript: Control whether enable the in post rendering JavaScript.enableAskYukina: Control whether enable the assistant. Please make it false because this function is still under development.footName: The string display in the footer.urlPrefix: Change the prefixing URL of the blog, default is an empty string.images.indexBannerImage: The banner image of index page.images.lazyloadImage: The placeholder image of images in the post.images.timelineBannerImage: The banner image of timeline page.
Basically, you only need to change title and motto and footName.
After installing all dependencies, you just execute api_server/roselia.py build to build the front-end.
Just execute api_server/roselia.py serve to start the server.
Then, configure the nginx or apache.
After that, access the blog index, follow the instructions, you are all done.
roselia.py provided some commands to start or build the blog. Usage: roselia.py [command]
command could be:
- serve: Start the service regarding
DEBUGsetting.- run-dev: Force start the development environment.
- run-prod: Force start the production environment.
- run-gunicorn: Start the server using
gunicorn.- compress-assets: Remove previous static folder,.copy static_assets
tostatic`, and compress images.- copy-assets Copy built front-end assets to static, then replace CSS and JS in templates.
- build-frontend Build the front end.
- build = compress-assets + build-front-end + copy-assets
- assets = compress-assets + copy-assets
If you are writing codes or formulas in articles, this blog is suitable for you because this blog has native, out-of-the-box support of following functions:
- Code highlighting.
- Formula
- Side-bar navigation.
- Preview of links to articles.
- References skip and preview.
- In-article JavaScript and string interpolation, for additional functions or provide support for comments.
- Block text template syntax for to humorously blocking out “inappropriate” or “secret” text. They are texts surrounded by
~. - Paste or drop to upload images.
- Paste code snippets directly from
Visual Studio Code. - Hidden Posts: posts which could only be accessed via permanent link.
- Secret Posts: posts which users with a certain role level only could access.
- Social login: user could login via GitHub or Microsoft accounts.
- Two factor authentication.
Roselia-Blog does not open for registration, hence, we fully trust every user in this site. So, we open the in-post or comment script to all users. This script could interpolate strings, open/close some switches, or programmatically change the metadata of post or comment. It is still dangerous because while (true) {} will still crash the browser. So, you could disable it in the config.
Syntax: <prefix>{{<expression>}}
The <prefix> is anything matches Roselia|roselia|r|R. The <expression> is a valid JavaScript expression.
If this expression ends with comma(;), this expression is treated as a clause and the execution result will be dropped.
Example: r{{ 1 + 1 }} renderes to 2.
There are such built-in apis:
def (name: string, func: any)naming a result. This result is possible to be used in following scripts or comments.onceLoad (fn: () => void)Add a callabck when post is loaded.onceUnload(fn: () => void)Add a callback when post is destroyed.getElement (name: RSElementSelector): HTMLElement | null: Get the HTML element by id or other api rendering result.element (name: RSElementSelector): Promise<HTMLElement>: Promise version ofgetElement.then (f: () => void): Add a callback when the DOM is loaded.music (meta: MusicMetaObject | MusicMetaObject[], autoplay = false, onPlayerReady?: (ob?: object) => void): Insert a music or musics (based on APlayer).btn (text: string, onClick?: () => void, externalClasses: Array<String>|String = ‘’, externalAttributes?: object): Insert a button.toast(text: string, color: string)Display a notification.importJS (url: string, onComplete?: any): Insert a JavaScript from other source. (May affect articles afterwards)createElement<K extends keyof HTMLElementTagNameMap>( type: K, extend?: RecursivePartial<HTMLElementTagNameMap[K]>, children?: (Node | string)[] ): HTMLElementTagNameMap[K]: Create the element with typetype, extend the attributed inextent, finally, fill the children.createTextNode(text: string): Text: Create a text node.currentTheme(): Get current theme palette.changeThemeOnce(theme: Partial<typeof config.theme>): void: Change the theme when viewing this post, reset theme when switching posts.changeTheme(theme: Partial<typeof config.theme>): void: Change the theme until refresh. (Asks forthemepriviledge).resetTheme(): Reset the theme to default.saveCurrentTheme(): Save current theme.switchToColorMode(isLight: boolean): Change the color mode. To light mode if isLight is true, otherwise dark mode. Reset after post is destroyed.async forceSwitchToColorMode(light: boolean): Change the color mode. Won’t change until refresh.changeExtraDisplaySettings(settings: Partial<{ metaBelowImage: boolean, blurMainImage: boolean, disableSideNavigation: boolean }>): Change the extra display settings.metaBelowImagecontrols whether the metadata of post is below the image.blurMainImagecontrols if blur the dominant image.disableSideNavigationcontrols whether disable the side navigation.sendNotification(notification: INotification): Send a notification to user (via the global notification bus).hyperScriptis a convenient shortcut for creating elements without passing children as array. Also, HTML tags can also be used as an attribute to be element creaters. In most cases, we aliasing this value ashto make our code shorter viadef('h', hyperScript); Example:
defState('userName', ''),
hyperScript.div(
hyperScript.h1('Hello', ' ', 'World!'),
hyperScript.span({
className: 'heimu'
}, 'This content is hidden.'),
'Who are you?',
hyperScript('input', {
value: userName,
onInput() {
userName = this.value;
}
}),
hyperScript('button', {
onClick() {
userName = '';
toast('Submitted!', 'success')
}
}, 'Submit')
)Roselia-Script supports react-like hooks, which is inspired both by Vue and React.
There are several APIs help building reactive posts.
APIs introducing new variables are prefixed with def. Hook APIs are prefixed with use. So, we have following APIs:
declare function defState<S>(name: string, state: S | (() => S)): voiddefState accepts a name and a state, introducing that variable to current article.
Basically, you could write a simplest counter in this way:
r{{
defState('count', 0), btn(count, () => ++count)
}}
This would introduce a button, displaying current count, increasing count every click.
Somebody would miss the legacy useState hook in React. Here is useState.
This hook is expected to hehave the same as React State Hook.
declare function useState<S>(state: S | (() => S)): [S, (value: (S | ((oldValue: S) => S))) => void]A counter could be written as:
(() => {
const [count, setCount] = useState(0)
return btn(count, () => setCount(c => c + 1))
})()Or:
def(['count', 'setCount'], useState(0)), btn(count, () => setCount(c => c + 1))defState is more reactive and short while useState is more functional. If you loved Vue, you might be happy with defState, otherwise use useState.
This hook is expected to behave the same as React Effect Hook.
declare function useEffect(effect: () => void, deps: any[]): voiddeclare function useInterval(callback: () => void, interval: number | null): voidBoth hook have the same signature. Interval could be changed when interval changed.
If interval is null, the interval stopped.
Here is an implementation to demonstrate how it works:
function useInterval(callback: () => void, interval: number | null): void {
useEffect(() => {
if (typeof interval === 'number') {
const timer = setInterval(() => fn(), interval)
return () => clearInterval(timer)
}
}, [interval])
}declare function useReactiveState<S extends object>(init: S | (() => S)): S;This hook takes an initial state, then returns a reactive proxy (not recursively listened). Then Roselia-Script will listen to its property changes, each change will cause a refresh. Since it is not deeply listened, make sure to mutate the value in the first layer.
declare function useMemo<S>(compute: () => S, deps: any[] = []): S;
function useCallback(callback: () => void, deps: any[]) {
return useMemo(() => callback, deps)
}useMemo prevents duplicate computing of time-intensive works. It takes the computing function which takes no argument and produces the value. This value will be memorized each call until elements of deps changes.
interface IRoseliaScriptContext<T> {
Provider: (props: { value: T }) => RoseliaVNode
}
declare function createContext<T>(defaultValue: T): IRoseliaScriptContext<T>;
declare function useContext<T>(context: IRoseliaScriptContext<T>): T;To use context, first you should create a context, then render a context provider component just like what React does.
In contrast, we do not have a consumer and function as props in Roselia-Script,
instead we use just useContext hook.
useContext hook takes a context (not its provider), and returns the contextual value, if not context found, the default value will be produced.
const ThemeContext = createContext(null);
const ThemedButton = ({text}) => {
const theme = useContext(ThemeContext)
return hyperScript.button({
style: {
background: theme.primary
},
}, text)
}
const App = () => {
return hyperScript(
ThemeContext.Provider,
{
value: {
primary: '#6670ed'
}
},
hyperScript(ThemedButton, {
text: 'Themed Button'
})
)
}There are two different procedures when processing in-article scripts. They are:
-
Rendering
Rendering relaces all scripts to its execution result as a single HTML string, then releace the post with this single HTML string. Whenever a state changed, the content will be re-rendered and all contents of this article will be replaced. This procedure is not so efficient during frequent state changes. This is the default processing method for articles and comments.
-
Mounting
This method compiles the article to a function yielding a virtual dom, just as popular MVVM frameworks. A new virtual dom will be generated everytime a state changed. After that, this virtual dom will be compared with old virtual dom via a reconciliation algorithm by diffing both doms, generating a patch, which actually changes the dom and this action is async. This procedure will not be so effcient if no state is going to be changed. This method is only available when rendering an article, to use this metod, you should add such content on the first line of article:
---feature:roselia-dom---This function is experimental and way of activiation may be changed.
Inserting a song:
r{{
music({
title: ‘陽だまりロードナイト’,
author: ‘Roselia’,
url: ‘https://cdn.roselia.moe/static/img/roselia/hidamari.mp3’,
pic: ‘https://p4.music.126.net/gT4F8nlV2Io58GTVAEWyLw==/18636722092789001.jpg’
})
}}
When the script is executed in posts, a special variable post will be available in the context.
Properties of this object can be modified to change the metadata during runtime. Also, a comment variable will be available when rendering comments.