Benjamin Frost
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights New
    • Engagement control
    • Make a copy
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Note Insights Versions and GitHub Sync Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control Make a copy Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       Owned this note    Owned this note      
    Published Linked with GitHub
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    # Technik Genau! Nun möchten wir über unseren Teil zur Technik reden. Dabei gehen wir auf unser Architektur-Diagramm ein, stellen dabei unseren Tech-Stack vor, sprechen über einige Design-Entscheidungen, die wir getroffen haben und reflektieren schließlich über die gewählten Technologien. ## Tech-Stack & Architektur Unsere Kern-Applikation besteht aus einem NestJS-Backend, einem VueJS-Frontend und zusätzlich einem Microservice zur PDF-Generierung. Diese Services laufen alle jeweils in einem Docker Container, den wir über ein Kubernetes Cluster deployen und orchestrieren. Außerdem gibt es eine Postgresql-Datenbank und Keycloak fürs Identity- und Access-Management. Dieser Tech-Stack war von Leo vorgegeben, damit er uns während der Entwicklung bei Fragen unterstützen kann und er das Projekt nach dem Ende selber warten kann. Auf unsere Erfahrungen mit den Technologien werde ich am Ende dieses Teils noch einmal kurz eingehen. Zudem gibt es noch einen PDF-Generator Microservice, welchen wir ebenfalls mit NestJS aufgesetzt haben. Dieser Service kann nur vom Backend aus erreicht werden und alle von ihm ausgehenden Requests werden von Kubernetes blockiert. Das hängt mit Sicherheitsproblemen der PDF-Generierung zusammen, worauf wir später noch einmal eingehen werden. KLICK Wie ihr vielleicht bemerkt habt, haben wir bei all diesem Komponenten auf Docker zurückgegriffen. Zum einen natürlich wegen der generellen Vorteile von Docker, wie erhöhte Portabilität und effektive Ressourcennutzung, aber wir lassen diesen Hauptkern auch innerhalb einer Kubernetes-Instanz laufen. Durch Kubernetes haben wir unter anderem Load-Balancing und die Möglichkeit Rolling-Updates und Deployment-Previews durchzuführen. KLICK Kubernetes ist derzeit so aufgesetzt dass von unserer Kern-Applikation zwei Instanzen gestartet werden. Über einen Ingress-Controller kann der Nutzer nun mit unserer Applikation sprechen und wird automatisch an die passende Instanz weitergeleitet. ## Aufgabenerstellung und PDF-Generierung | Ablauf Wir möchten uns nun mit dem Ablauf der Aufgabenerstellung sowie der PDF-Generierung eines Arbeitsblattes beschäftigen. Unsere Applikation deckt natürlich viele verschiedene Use-Cases und Funktionen ab, aber der Grund warum wir genau über diese sprechen möchten ist, dass sie die Kernfunktionen sind und wir viele größere Diskussionen über den genauen Vorgang und die technische Umsetzung hatten. Bevor wir allerdings auf diese Diskussionen und Entscheidungen zu sprechen kommen, möchten wir nun den Ablauf vorstellen. ## Aufgabenerstellung | Ablauf Schauen wir uns zunächst die Aufgabenerstellung an. Wie gesagt beginnt der Prozess damit, dass ein Nutzer auf den entsprechenden Knopf im Frontend drückt. Dies löst im Hintergrund nun einen asynchronen Aufruf an unser Backend aus. Dabei wird über einen POST-Reqest die zu erstellende Aufgabe übergeben und vom Backend in der Datenbank gespeichert. Das klingt zwar sehr simpel, aber dennoch gab es dabei einiges an Diskussionsbedarf, insbesondere bei der Entscheidung, wie Nutzer den Inhalt eingeben. Wir wussten, dass wir schließlich den Inhalt in ein Format bringen müssen, dass wir in PDF umwandeln können. Dabei gibt es allerdings verschiedene Möglichkeiten. Entweder man erzeugt dieses PDF-fähige Format bereits auf Client-Seite oder erst später bei der PDF-Generierung. ## Aufgabenerstellung | Richtext-Editor Unsere initiale Idee war es, einen Richtext-Editor zur Erstellung von Aufgaben zu nutzen. Es würde nur wenig Arbeitszeit kosten einen Richtext-Editor zu integrieren, da viele große Bibliotheken dafür verfügbar sind und eine einfache Einbidung ermöglichen. Darüber hinaus arbeiten diese Editoren nach dem "What You See Is What You Get"-Prinzip, was zu einer guten User-Experience führt, da Nutzer direkt sehen, wie ihre Aufgabe am Ende auch auf der generierten PDF aussieht. Super! Damit sind alle Probleme gelöst und wir sind mit allem fertig. Das dachten wir für wenige Sekunden, bis jemand das Todschlag-Argument der "Mathe-Pyramide" in die Diskussion eingeworfen hatte. Es handelt sich dabei um eine der komplexeren Aufgabenkonstrukte, die wir unterstützten wollten. Alle Vorschläge, wie man eine Mathe-Pyramide gut in diese Editoren integrieren könnte, verliefen im nichts. Mit keiner Lösung waren wir zufrieden und schließlich mussten wir feststellen, dass wir langfristig nur Probleme mit diesen Editoren bekommen werden. Der Hauptgrund dafür ist deren eingeschränkte Flexibilität. Zwar können wir eigene Knöpfe hinzufügen und den Editor beliebig erweitern, aber wir sind lediglich auf das HTML-Format beschränkt und das was wir innerhalb dessen machen können. Darüber hinaus hat uns Leo darauf hingewiesen, dass diese Editoren sehr schnell zu unsauberem HTML führen, z.B. wenn man Inhalt von anderen Webseiten versucht zu Copy&Pasten. Entscheidend ist an diesem Punkt anzumerken, dass wir uns durch einen Richtext-Editor endgültig auf das Format HTML festlegen würden. Im vorherigen Teil hatten wir kurz erwähnt, dass wir durch unser flexibles Datenbankformat beliebig wechseln können zwischen HTML, LaTeX und anderen Formaten. Wenn wir allerdings den Inhalt bereits als HTML in der Datenbank festankern, ist dies nicht mehr möglich. ## Aufgabenerstellung | Templates So sind wir schließlich auf die Idee gekommen, lediglich den semantischen Inhalt der Aufgaben abzuspeichern. Wie im Bild oben rechts zu erkennen ist, haben wir uns dabei für JSON als Wrapper um unser eigenes Format entschieden. Man kann sich das ganze nun so vorstellen, dass wir zunächst mittels dem Property "type" beschreiben, was wir speichern möchten und dann mit dem "content" was der Inhalt ist. Der Fokus liegt hier also wirklich auf dem "was" und nicht auf dem "wie". Wir haben versucht die Kopplung zwischen Inhalt und Format möglichst stark aufzuheben. Dies ermöglicht uns eine deutlich stärkere Kontrolle über die Struktur der gespeicherten Aufgaben und flexibel zwischen verschiedenen Formaten zu wechseln. Der größte Nachteil ist allerdings, dass dieser Eigenbau mit einem massiven Aufwand verbunden ist. Wir können hier nicht einfach die Bibliothek eines großen Open-Source-Projekts einbinden und sind fertig. Wir müssen uns sowohl Format als auch den Eingabe-Editor überlegen und entwickeln. Darüber hinaus müssen wir im Backend ebenfalls eine Umwandlung des eigenen Formats in ein Format, das zur PDF-Generierung genutzt werden kann, durchführen. Da die Vorteile die Nachteile insgesamt allerdings überwiegen, haben wir uns – wie ihr vielleicht schon raushört – für diese Lösung entschieden. ## PDF-Generierung | Ablauf Nun betrachten wir den zweiten Teil dieses Sequenzdiagramms. Dabei gehen wir davon aus, dass der Nutzer nun einige Aufgaben erstellt, zu einem Arbeitsblatt hinzugefügt hat und sich nun das passende PDF dazu generieren möchte. Zunächst klickt der Nutzer ähnlich wie im ersten Teil auf den entsprechenden Knopf im Frontend. Unser Backend empfängt diese Anfrage auf Controller-Ebene und startet auf Service- bzw. Business-Logik-Ebene nun die eigentliche Generierung. Da für die Generierung natürlich der Inhalt und weitere Meta-Daten des Arbeitsblattes benötigt werden, holt sich das Backend die entsprechenden Daten aus der Datenbank. Von dort erhält das Backend die Daten allerdings noch in dem semantischen Format, in dem sie zur Generierung unbrauchbar sind. Wir haben uns dazu entschieden, die Arbeitsblätter in HTML für die Generierung umzuwandeln. Sobald das endgültige HTML generiert worden ist, kann es an unseren PDF-Microservice geschickt werden. Mittels einer Headless Chrome-Browser Instanz, in unserem Fall von Puppeteer, wird nun das HTML in PDF umgewandelt und anschließend wieder an unser Backend gesendet. ****Der Backend-Service hat nun die vom Nutzer angefragte Ressource vorliegen und kann sie zurück an den Controller reichen. Der Controller muss schließlich nur noch entsprechende Header für die Response setzen, damit das Frontend diese korrekt empfangen kann und kann die Anfrage dann abschließen. Das Frontend erhält das PDF und kann es dem Nutzer präsentieren.**** ## Arbeitsblatt-Generierung | Content-Format Wir haben gerade erwähnt, dass wir die in der Datenbank gespeicherten Daten vor der PDF-Generierung in HTML umwandeln. Wenn wir über die Formate der Arbeitsblatt-Generierung gesprochen und diskutiert haben, stand dabei vor allem immer das Eingabe- und Speicherungsformat im Vordergrund. Das Ausgabeformat stand im Endeffekt seit Tag 1 fest. PDF bietet viele Vorteile, wie bspw. dessen Portabilität, einfache Druckbarkeit, Unmengen an nützlichen Libraries, Generierungs-Tools und schließlich war PDF auch einfach unser Kundenwunsch. Die Entscheidung über das Eingabeformat ist uns leider signifikant schwerer gefallen. Hauptsächlich standen dabei HTML und LaTeX zur Debatte. Die beiden großen Vorteile an HTML sind, dass es wirklich sehr leicht zu erstellen ist und wir bereits nach kurzer Zeit vielversprechende Ergebnisse erzielen könnten. Darüber hinaus könnten wir den HTML-Inhalt ebenfalls direkt dem Nutzer als Live-Preview anzeigen, da Browser dies nativ verstehen würden. Das führt allerdings zu vielen Sicherheitsproblemen insbesondere auf der Serverseite. Wir müssen sehr genau überprüfen was Nutzer an unseren Server übermitteln, die Eingaben bereinigen, darüber hinaus noch den Headless Browser von unserem Backend entkoppeln und somit unser Netzwerk absichern. Darüber hinaus ist HTML leider in seiner Funktionalität sehr beschränkt. Wir haben uns im Projekt natürlich mit der "Legacy-Arbeitsblatt-Generierung" beschäftigt, welche aus Libre-Office und Kopiererei bestand. Viele Dinge, die auf diese Weise erstellt worden sind, waren mit HTML zwar leicht umsetzbar, andere hingegen nur mit enormen Aufwand und sehr "hacky" Lösungen. Ein Beispiel was in Diskussionen um HTML jedes Mal gefallen ist, sind die altbekannten Mathe-Pyramiden. Auf der Suche, wie man diese mit HTML umsetzen könnte, sind wir mehrfach auf LaTeX gestoßen und haben gesehen wie leicht dies mit LaTeX umsetzbar wäre. Gerade wenn es darum geht komplexe Strukturen umzusetzen würden wir sehr von LaTeX profitieren und einfache Strukturen sind ja ohnehin kein Problem. Diese Vorteile sind allerdings mit einem höheren initialen Aufwand verbunden und vom Entwickler werden nun neben HTML auch noch LaTeX-Kenntnisse abgefordert. Dass wir mi LaTeX keine Chrome Sandbox benötigen würden ist ein Pro in der Debatte um Sicherheit, allerdings müssen wir ehrlich gestehen, dass es bei LaTeX garantiert andere Sicherheitsrisiken gibt. Mit diesen haben wir uns nur nicht so genau außeinandergesetzt, da wir uns am Ende auf die HTML-Variante fokussiert haben. Vor allem aus dem Grund, dass wir erst sehr spät im Kurs mit der Arbeitsblatt-Generierung begonnen haben, haben wir uns schließlich dazu entschieden HTML zu verwenden. Um allerdings nicht endgültig auf diesem Format fixiert zu sein, verwenden wir in der Datenbank ein Speicherungsformat, was unabhängig von konkreter Syntax ist und sich einzig und allein auf die Semantik beschränkt. Dies erlaubt uns auch problemlos auf LaTeX umzustellen oder eine hybride Variante anzustreben, sollte es notwendig werden. ## Arbeitsblatt-Generierung | Sicherheitshürden Wie wir schon vorhin angesprochen hatten, gab es bei der PDF-Generierung der Arbeitsblätter einige Sicherheitsaspekte, auf die wir erst nach der Implementierung aufmerksam wurden. Wir haben hier zwei Beispiele mitgebracht, die potenzielle Exploits unserer PDF-Generierung zeigen und was für entsprechende Maßnahmen wir dagegen ergriffen haben. Die erste Sicherheitshürde, die wir umgehen mussten, war eine typische HTML-Injection. Mit dieser Technik konnte man beliebiges HTML in den Titel oder Body des Arbeitsblattes schreiben. Dies wird vom Backend in das PDF Template hereingegeben. Hiermit war es zum Beispiel möglich, Bilder, Bold-Text oder andere ungewünschte HTML-Tags in das Arbeitsblatt zu schreiben. Das sind noch relativ harmlose Beispiele, wenn man sich überlegt, dass darüber auch ein Script-Tag injiziert werden kann, über das man JavaScript-Code ausführen konnte. Der Höhepunkt war damit erreicht, dass wir Puppeteer ohne eine Browser-Sandbox gestartet haben, wodurch man sogar auf den Server zugreifen konnte. Glücklicherweise konnten wir dies mit relativ wenig Umstand durch eine sanitization-library umgehen, welche sämtliche HTML tags bzw. Symbole aus dem Content entfernt. Der zweite Sicherheitsaspekt, welcher ebenfalls bei der Generierung aufgetreten ist, war eine mögliche Server-Side-Request-Forgery-Attacke. Dabei war es möglich über den gespeicherten Inhalt auf interne Ressourcen des Clusters zuzugreifen. Dies wurde uns damit demonstriert, dass man plötzlich auf Bilder, die innerhalb des internen Bereichs liegen, zugreifen konnte. Das haben wir gelöst, indem wir die PDF-Generierung in einen eigenen Microservice ausgelagert haben, der nur sehr beschränkt im Netzwerk kommunizieren kann. Vor allem haben wir zum Thema Sicherheit gelernt, dass man sich eigentlich immer auf das schlimmste einstellen sollte, wenn man mit user-generiertem Content arbeitet, da es nur eine Frage der Zeit ist, bis jemand eine Sicherheitslücke entdeckt. ## Reflexion Wir möchten abschließend reflektieren und darüber sprechen, was aus technischer Sicht gut lief und was verbesserungswürdig gewesen wäre. Grundsätzlich sind wir mit der Wahl der Frameworks, der Architektur und den meisten Entwicklungsentscheidungen sehr zufrieden. Einige Dinge sind uns allerdings aufgefallen, die wir in einem zukünftigen Projekt wahrscheinlich anders machen würden. Als ersten Punkt geht es dabei um Sicherheit. Im Großteil des Projekts haben wir uns mit diesem Thema überhaupt nicht auseinandergesetzt. Erst als dann Sicherheitsprobleme auftraten - und Leo uns glücklicherweise darauf aufmerksam gemacht hat - haben wir uns damit beschäftigt. Da es nun um schon fertig implementierte Features ging, in denen bei uns Sicherheitslücken auftraten, mussten wir uns ein zweites Mal mit diesen befassen. Als Lehre haben wir deshalb daraus gezogen, konsequent von Anfang des Projekts bei jeder funktionalen Erweiterung den Sicherheitsaspekt mit zu betrachten. So hätten wir den Aufwand für Features im Vorhinein besser einschätzen, und - viel wichtiger - Sicherheitslücken vorbeugen können. Bei der Entwicklung von IDEA haben wir dadurch unerwarteten Aufwand im Nachhinein gehabt, um entsprechende Stellen zu fixen bzw. refactoren. KLICK Der zweite Punkt der besser hätte laufen können, war das automatisierte Testen unserer Applikation. Wir hatten darauf plädiert, keine Tests im Frontend zu schreiben, was zum Beispiel dazu geführt hat, dass mehrere Monate lang unser LogOut Knopf nicht funktioniert hat. Und das hat auch keiner bemerkt. Mit automatisierten Tests wären wir unmittelbar darauf aufmerksam gemacht worden, als der Knopf nicht mehr funktioniert hat. User Interfaces zu testen ist immer eine schwierige Diskussion, die die meisten Entwickler gar nicht führen wollen, aber wie man sieht, hat das schon seinen Sinn. In Zukunft würden wir zumindest für grundlegende Funktionalitäten auch im Frontend Tests schreiben. Dazu gibt es auch angemessene Frameworks wie Cypress, mit denen Teile unseres Teams sogar schon Erfahrung haben. Im Backend hatten wir zwar Tests, allerdings noch nicht von Anfang an. Das Aufsetzen der Testsuite komplizierter war als gedacht, da wir eine Keycloak- und Datenbank-Anbindung in den Tests sicherstellen mussten, und am Anfang andere funktionale Features höher als die Tests priorisiert hatten. Als wir dann die Tests aufgesetzt hatten, ist uns aufgefallen, dass es Fehler in Code gab der schon lange deployt war, was zu unerwartetem Debuggingaufwand gegen Ende der Zeit geführt hat. Unsere Learnings hier sind es die Testumgebung so früh wie möglich aufzusetzen und die Tests dann parallel mit den Features zu schreiben, oder sogar TDD zu verwenden - sprich Tests für Funktionen zu schreiben, bevor wir sie überhaupt programmieren. Dementsprechend wüssten wir dann sofort wann die Funktionalität richtig implementiert ist, da dann auf einmal die Tests durchlaufen. KLICK Der letzte Punkt den wir hier aufführen möchten, ist ein eher kleiner Kritikpunkt, bei dem wir uns auch noch nicht sicher sind ob andere Alternativen so viel besser gewesen wären. Es geht hier um die Art wie wir End-To-End-Type-Safety sichergestellt haben. Auf Wunsch von Leonard haben wir dafür im Backend mit Swagger alle Schnittstellen dokumentiert. Das Frontend hat allerdings keinen Zugriff auf den Code im Backend, weshalb wir aus der Swagger-Spezifikation aus dem Backend einen Api-Client generieren müssen, den das Frontend dann verwendet und in dem ebenfalls alle Schnittstellen spezifiziert sind. Das kleine Problem das im Zuge dieser Methode auftrat war, dass der Client bei jeder Änderung des Kommunikationsprotokolls im Backend neu generiert werden musste. Das hat immer etwas Zeit gekostet und außerdem dafür gesorgt, dass unsere CI-Pipelines langsamer laufen, weil auch dort der Client immmer neu generiert werden muss. Eine Verbesserung die wir hätten nutzen können ist tRPC, ein Projekt das sich genau diesem Problem widmet. Ein anderer Gedanke war es, unseren Package-Manager yarn so zu konfigurieren, dass das Front- und Backend einen gemeinsamen Workspace teilen, in dem solche Informationen ähnlich wie in einem Common-Modul vermerkt sind. Zusammenfassend konnten wir also sehr viel aus dem Projekt lernen und glauben, dass trotz der genannten Punkte die rein technische Umsetzung überwiegend erfolgreich verlaufen ist.

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password

    or

    By clicking below, you agree to our terms of service.

    Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully