seagull-js/seagull

View on GitHub
packages/pages/src/Page.tsx

Summary

Maintainability
A
1 hr
Test Coverage
import { isString } from 'lodash'
import * as React from 'react'
import { hydrate } from 'react-dom'
import { getStyles, setStylesTarget } from 'typestyle'
import { getTray, TTray, TTrayItem } from './bodytray'
import { Helmet } from './helmet'

export type PageType = { new (...args: any[]): Page }

/**
 * Props for the [[Page]] constructor, representing a routing state. They will
 * get filled in from the React renderer and can be manipulated when
 * instancing a [[Page]] class directly in unit tests.
 */
export interface IPageProps {
  data: any
}

/**
 * A highly convenient React component that contains all bells and whistles
 * to represent a complete HTML site. You have to define a [[path]] where the
 * [[Page]] will be rendered on and a [[html]] method rendering some HTML, the
 * other things are strictly optional. Example:
 *
 * ```typescript
 import { MetaData, Page } from '@seagull/framework'
 import * as React from 'react'

 export default class GreetPage extends Page {
   title: MetaData<GreetPage> = page => `<h1>Greetings, ${this.props.params.name}`
   html () {
     return <h1>Greetings, {this.props.params.name}</h1>
   }
 }
 ```
 */
export abstract class Page<P = {}, S = {}> extends React.Component<
  P & IPageProps,
  S
> {
  /**
   * setup the current page in the browser
   */
  static bootstrap() {
    const CurrentPage = (window as any).Page.default
    const data = (window as any).__initial_state__
    hydrate(<CurrentPage data={data} />, document.querySelector('#app'))
    setStylesTarget(document.getElementById('styles-target')!)
  }

  /**
   * This method returns the styles of the typestyle library by default. It can be overwritten if another css source will be used.
   */
  static getStyles(): string {
    return getStyles()
  }

  static getTray(tray: TTray): TTrayItem {
    return getTray(tray)
  }

  /**
   * get the used helmet instance for SSR
   */
  static helmetInstance() {
    return Helmet
  }

  /**
   * If you want to set the `<title />` in the head of the HTML website, set
   * this property to the desired string. If you optimize for SEO, the length
   * of the title string must not exceed 65 characters and should contain the
   * exact same textual content as  the `<h1 />` tag within the page body.
   * If you need dynamic data for the string, you can also pass in a callback
   * function. Example code:
   *
   * ```typescript
   // static:
   title = 'my site title'
   // dynamic:
   title = () => `my site with id: ${this.props.params.id}`
   ```
   */
  title: string | (() => string) = ''

  /**
   * If you want to set the `<meta name="description" />` in the head of the
   * HTML website, set this property to the desired string.
   * If you optimize for SEO, the length of the description string must not
   * exceed 160 characters.
   * If you need dynamic data for the string, you can also pass in a callback
   * function. Example code:
   *
   * ```typescript
   // static:
   description = 'my site description'
   // dynamic:
   description = () => `my site with id: ${this.props.params.id}`
   ```
   */
  description: string | (() => string) = ''

  /**
   * If you want to set proper image meta tags for social media in the head of
   * the HTML website, set this property to the desired string.
   * If you need dynamic data for the string, you can also pass in a callback
   * function. Example code:
   *
   * ```typescript
   // static:
   image = '/path/to/image.jpg'
   // dynamic:
   image = () => `/path/to/{this.props.params.image}.jpg`
   ```
   */
  image: string | (() => string) = ''

  /**
   * Authors are encouraged to specify a lang attribute on the root html
   * element, giving the document's language. This aids speech synthesis
   * tools to determine what pronunciations to use, translation tools to
   * determine what rules to use, and so forth.
   */
  language: string | (() => string) = 'de'

  /**
   * This is the "robots" meta tag and defaults to 'index, follow'. CHange it if
   * you want to control the indexation of your pages from search engines.
   * Example code:
   *
   * ```typescript
   // static:
   robots = 'index,follow'
   // dynamic:
   robots = () => `index, {this.props.params.follow ? 'follow' : 'nofollow'}.jpg`
   ```
   */
  robots: string | (() => string) = 'index,follow'

  /**
   * This is the 'canonical' meta tag, pointing to the origin of a page. Use it
   * to transfer SEO value to another page or to cut out query parameters for
   * your [[Page]]. Defaults to `this.props.path`.
   * Example code:
   *
   * ```typescript
   // static:
   canonical = '/some/path'
   // dynamic:
   canonical = () => this.props.path
   ```
   */
  canonical: string | (() => string) = ''

  /**
   * Implement this method for your actual page content instead of the typical
   * [[render]] method in react. Seagull *will* call [[render]] under the hood
   * but with additional bells and whistles attached.
   */
  abstract html(): JSX.Element

  /**
   * DO NOT IMPLEMENT THIS IN YOUR CODE. Use the [[html]] method instead.
   */
  render() {
    const html = this.html.bind(this)
    const rendered = html()

    const prop = (p: any): string => (isString(p) ? p : p.bind(this)())
    const title = prop(this.title) || this.constructor.name
    const description = prop(this.description) || ''
    const image = prop(this.image) || ''
    const link = prop(this.canonical) || ''
    const language = prop(this.language) || 'de'

    return (
      <>
        <Helmet htmlAttributes={{ lang: language }}>
          {/* general meta tags */}
          <meta charSet="utf-8" />
          <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
          <meta
            name="viewport"
            content="width=device-width, initial-scale=1.0, maximum-scale=1.0"
          />

          {/* social media classification tags */}
          <meta name="twitter:card" content="summary" />
          <meta name="og:type" content="website" />

          {/* title meta tags */}
          <title>{title}</title>
          <meta name="twitter:title" content={title} />
          <meta name="og:title" content={title} />

          {/* description meta tags */}
          <meta name="description" content={description} />
          <meta name="og:description" content={description} />
          <meta name="twitter:description" content={description} />

          {/* image meta tags */}
          <meta name="og:image" content={image} />
          <meta name="twitter:image" content={image} />

          {/* URL meta tags */}
          <meta name="og:url" content={link} />
          <meta name="twitter:url" content={link} />
          <link rel="canonical" href={link} />
        </Helmet>
        {rendered}
      </>
    )
  }
}