مساعد SSG

ينشئ SSG Helper موقعًا ثابتًا من تطبيق Hono الخاص بك. سيسترجع محتويات المسارات المسجلة ويحفظها كملفات ثابتة.

الاستخدام

يدوي

إذا كان لديك تطبيق Hono بسيط كما يلي:

// index.tsx
const app = new Hono()

app.get('/', (c) => c.html('Hello, World!'))

app.use('/about', async (c, next) => {
  c.setRenderer((content) => {
    return c.html(
      
        <head />
        <body>
          <p>{content}</p>
        </body>
      
    )
  })
  await next()
})

app.get('/about', (c) => {
  return c.render(
    <>
      <title>Hono SSG Page</title>Hello!
    </>
  )
})

export default app

بالنسبة لـ Node.js، أنشئ سكربت بناء كهذا:

// build.ts
import app from './index'
import { toSSG } from 'hono/ssg'
import fs from 'fs/promises'

toSSG(app, fs)

بتنفيذ السكربت، ستُخرَج الملفات كما يلي:

ls ./static
about.html  index.html

إضافة Vite

باستخدام @hono/vite-ssg Vite Plugin، يمكنك التعامل مع العملية بسهولة.

لمزيد من التفاصيل، انظر هنا:

https://github.com/honojs/vite-plugins/tree/main/packages/ssg

toSSG

toSSG هي الدالة الرئيسية لتوليد المواقع الثابتة، حيث تأخذ تطبيقًا ووحدة نظام ملفات (filesystem) كمعاملات. وهي مبنية على ما يلي:

المدخلات

تُحدَّد وسائط (arguments) toSSG في ToSSGInterface.

export interface ToSSGInterface {
  (
    app: Hono,
    fsModule: FileSystemModule,
    options?: ToSSGOptions
  ): Promise<ToSSGResult>
}
  • app يحدِّد new Hono() مع المسارات المسجلة.
  • fs يحدد الكائن التالي، بافتراض node:fs/promise.
export interface FileSystemModule {
  writeFile(path: string, data: string | Uint8Array): Promise<void>
  mkdir(
    path: string,
    options: { recursive: boolean }
  ): Promise<void | string>
}

استخدام المحوّلات لـ Deno و Bun

إذا كنت تريد استخدام SSG على Deno أو Bun، فإن toSSG الدالة مُوفّرة من قِبل @hono/deno و @hono/bun الحزم.

لـ Deno:

import { toSSG } from '@hono/deno'

toSSG(app) // The second argument is an option typed `ToSSGOptions`.

لـ Bun:

import { toSSG } from '@hono/bun'

toSSG(app) // The second argument is an option typed `ToSSGOptions`.

الخيارات

تُحدَّد الخيارات في واجهة ToSSGOptions.

export interface ToSSGOptions {
  dir?: string
  concurrency?: number
  extensionMap?: Record<string, string>
  plugins?: SSGPlugin[]
}
  • dir هي وجهة الإخراج للملفات الثابتة (Static files). القيمة الافتراضية هي ./static.
  • concurrency هو عدد الملفات المتزامنة التي سيتم توليدها في الوقت نفسه. القيمة الافتراضية هي 2.
  • extensionMap خريطة تحتوي على Content-Type كمفتاح وسلسلة الامتداد كقيمة. يُستخدم هذا لتحديد امتداد ملف الإخراج.
  • plugins هي مصفوفة من إضافات SSG التي توسع وظائف عملية توليد المواقع الثابتة.

المخرجات

toSSG يُعيد النتيجة في نوع Result التالي.

export interface ToSSGResult {
  success: boolean
  files: string[]
  error?: Error
}

إنشاء ملف

المسار واسم الملف

تنطبق القواعد التالية على معلومات المسار المسجلة واسم الملف المُنشأ. الافتراضي ./static يتصرف كما يلي:

  • / -> ./static/index.html
  • /path -> ./static/path.html
  • /path/ -> ./static/path/index.html

امتداد الملف

يعتمد امتداد الملف على Content-Type التي يُعيدها كل مسار. على سبيل المثال، الاستجابات من c.html يتم حفظها كـ .html.

إذا أردت تخصيص امتدادات الملفات، فاضبط extensionMap خيار.

import { toSSG, defaultExtensionMap } from 'hono/ssg'

// Save `application/x-html` content with `.html`
toSSG(app, fs, {
  extensionMap: {
    'application/x-html': 'html',
    ...defaultExtensionMap,
  },
})

لاحظ أن المسارات التي تنتهي بشرطة مائلة يتم حفظها باسم index.ext بغض النظر عن الامتداد.

// save to ./static/html/index.html
app.get('/html/', (c) => c.html('html'))

// save to ./static/text/index.txt
app.get('/text/', (c) => c.text('text'))

البرمجيات الوسيطة

تقديم وسيط مدمج يدعم SSG.

ssgParams

يمكنك استخدام واجهة API مثل generateStaticParams الخاص بـ Next.js.

مثال:

app.get(
  '/shops/:id',
  ssgParams(async () => {
    const shops = await getShops()
    return shops.map((shop) => ({ id: shop.id }))
  }),
  async (c) => {
    const shop = await getShop(c.req.param('id'))
    if (!shop) {
      return c.notFound()
    }
    return c.render(
      <div>
        <h1>{shop.name}</h1>
      </div>
    )
  }
)

isSSGContext

isSSGContext دالة مساعدة تُرجع true إذا كان التطبيق الحالي يعمل ضمن سياق SSG المُفعَّل بواسطة toSSG.

app.get('/page', (c) => {
  if (isSSGContext(c)) {
    return c.text('This is generated by SSG')
  }
  return c.text('This is served dynamically')
})

disableSSG

المسارات ذات disableSSG مجموعة الوسيطات (middleware) مُستثناة من توليد الملفات الثابتة بواسطة toSSG.

app.get('/api', disableSSG(), (c) => c.text('an-api'))

onlySSG

المسارات ذات onlySSG سيُتجاوَز طقم البرمجيات الوسيطة المضبوط بواسطة c.notFound() بعد toSSG التنفيذ.

app.get('/static-page', onlySSG(), (c) => c.html(<h1>Welcome to my site</h1>))

الإضافات

تسمح لك الإضافات (Plugins) بتوسيع وظائف عملية توليد المواقع الثابتة. تستخدم خطافات (hooks) لتخصيص عملية التوليد في مراحل مختلفة.

الإضافة الافتراضية

بشكل افتراضي، toSSG يستخدم defaultPlugin الذي يتخطى استجابات الحالة غير 200 (مثل عمليات إعادة التوجيه أو الأخطاء أو 404). يمنع هذا إنشاء ملفات للاستجابات غير الناجحة.

import { toSSG, defaultPlugin } from 'hono/ssg'

// defaultPlugin is automatically applied when no plugins specified
toSSG(app, fs)

// Equivalent to:
toSSG(app, fs, { plugins: [defaultPlugin] })

إذا حدّدت إضافات (plugins) مخصصة، defaultPlugin لا يتم تضمينه تلقائيًا. للاحتفاظ بالسلوك الافتراضي مع إضافة إضافات (plugins) مخصصة، قم بتضمينه صراحةً:

toSSG(app, fs, {
  plugins: [defaultPlugin, myCustomPlugin],
})

إضافة إعادة التوجيه

الـ redirectPlugin ينشئ صفحات إعادة توجيه HTML للمسارات التي تُرجع استجابات إعادة توجيه HTTP (301، 302، 303، 307، 308). يتضمن HTML الناتج <meta http-equiv="refresh"> وسم ورابط canonical.

import { toSSG, redirectPlugin, defaultPlugin } from 'hono/ssg'

toSSG(app, fs, {
  plugins: [redirectPlugin(), defaultPlugin()],
})

على سبيل المثال، إذا كان تطبيقك يمتلك:

app.get('/old', (c) => c.redirect('/new'))

الـ redirectPlugin سيُنشئ ملف HTML في /old.html مع إعادة توجيه meta refresh إلى /new.

[!NOTE] عند استخدامه مع defaultPlugin، ضع redirectPlugin before defaultPlugin. منذ defaultPlugin يتخطى الاستجابات غير 200، فإن وضعه أولاً سيمنع redirectPlugin من معالجة استجابات إعادة التوجيه.

أنواع الخطافات

يمكن للإضافات استخدام الخطّافات (hooks) التالية لتخصيص toSSG process:

export type BeforeRequestHook = (req: Request) => Request | false
export type AfterResponseHook = (res: Response) => Response | false
export type AfterGenerateHook = (
  result: ToSSGResult
) => void | Promise<void>
  • BeforeRequestHook: يُستدعى قبل معالجة كل طلب. أعد false لتخطّي المسار.
  • AfterResponseHook: يُستدعى بعد استلام كل استجابة. أعد false لتخطي إنشاء الملف.
  • AfterGenerateHook: يُستدعى بعد اكتمال عملية التوليد (generation) بأكملها.

واجهة الإضافة

export interface SSGPlugin {
  beforeRequestHook?: BeforeRequestHook | BeforeRequestHook[]
  afterResponseHook?: AfterResponseHook | AfterResponseHook[]
  afterGenerateHook?: AfterGenerateHook | AfterGenerateHook[]
}

أمثلة أساسية للإضافات

تصفية طلبات GET فقط:

const getOnlyPlugin: SSGPlugin = {
  beforeRequestHook: (req) => {
    if (req.method === 'GET') {
      return req
    }
    return false
  },
}

التصفية حسب رمز الحالة:

const statusFilterPlugin: SSGPlugin = {
  afterResponseHook: (res) => {
    if (res.status === 200 || res.status === 500) {
      return res
    }
    return false
  },
}

تسجيل الملفات المُنشأة:

const logFilesPlugin: SSGPlugin = {
  afterGenerateHook: (result) => {
    if (result.files) {
      result.files.forEach((file) => console.log(file))
    }
  },
}

مثال على إضافة متقدمة

فيما يلي مثال على إنشاء إضافة (plugin) لخريطة الموقع (sitemap) تُنشئ ملف sitemap.xml file:

// plugins.ts
import fs from 'node:fs/promises'
import path from 'node:path'
import type { SSGPlugin } from 'hono/ssg'
import { DEFAULT_OUTPUT_DIR } from 'hono/ssg'

export const sitemapPlugin = (baseURL: string): SSGPlugin => {
  return {
    afterGenerateHook: (result, fsModule, options) => {
      const outputDir = options?.dir ?? DEFAULT_OUTPUT_DIR
      const filePath = path.join(outputDir, 'sitemap.xml')
      const urls = result.files.map((file) =>
        new URL(file, baseURL).toString()
      )
      const siteMapText = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${urls.map((url) => `<url><loc>${url}</loc></url>`).join('\n')}
</urlset>`
      fsModule.writeFile(filePath, siteMapText)
    },
  }
}

تطبيق الإضافات:

import app from './index'
import { toSSG } from 'hono/ssg'
import { sitemapPlugin } from './plugins'

toSSG(app, fs, {
  plugins: [
    getOnlyPlugin,
    statusFilterPlugin,
    logFilesPlugin,
    sitemapPlugin('https://example.com'),
  ],
})